﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Xml;
using System.Xml.Linq;
using System.Globalization;

namespace ListItemEditor
{
    public class GridViewDataSourceWebSvc : IGridViewDataSource
    {
        #region constants

        private const int NUMBER_BATCH_ITEMS = 200;

        #endregion

        #region variables

        protected static readonly HashSet<string> SpecialFields = new HashSet<string>() { "FileLeafRef", "BaseName", "_ModerationStatus", "_Level", "owshiddenversion" };

        #endregion

        #region IGridViewDataSource Members

        public GridViewDataSourceSettings Settings { get; set; }

        public DataTable GetData()
        {
            ListData.ViewData view = null;

            ListData listData = WscHelper.Current.GetListData(this.Settings);
            if (listData == null) return null;

            if (!string.IsNullOrEmpty(Settings.ViewName)) view = listData.Views.Where(v => Settings.ViewName.Equals(v.Name)).FirstOrDefault();
            if (view == null) view = listData.DefaultView;

            if (view == null)
            {
                if (Settings.ViewFields == null || Settings.ViewFields.Count == 0) Settings.ViewFields = new List<string>() { "ID"};
            }
            else
            {
                this.Settings.ViewName = view.Name;

                ViewData viewData = WscHelper.Current.GetViewData(this.Settings, view.ID);
                if (viewData == null) return null;

                if (Settings.ViewFields == null || Settings.ViewFields.Count == 0) Settings.ViewFields = viewData.ViewFields;
                if (Settings.Query == null) Settings.Query = viewData.Query;
            }

            Settings.ViewFields = FixViewFieldSchema(listData, Settings.ViewFields);
            DataTable table = WscHelper.Current.GetListItemData(this.Settings, listData, Guid.Empty, true, this.Settings.RowLimit);
            table.AcceptChanges();

            return table;
        }

        public void ApplyDataChanges(DataTable table)
        {
            DataRow[] changedRows = table.Select(null, null, DataViewRowState.ModifiedCurrent | DataViewRowState.Added | DataViewRowState.Deleted);
            if (changedRows.Length == 0) return;

            this.UpdateItems(table, changedRows, false);
        }

        public void CheckOutInFiles(List<DataRow> selectedRows, CheckOutAction checkOutAction)
        {
            if (selectedRows == null || selectedRows.Count == 0) return;
            
            ListData list = WscHelper.Current.GetListData(this.Settings);
            List<DataRow> succeededRows = new List<DataRow>();

            string hostUrl = new Uri(this.Settings.WebUrl).GetComponents(UriComponents.SchemeAndServer | UriComponents.Port, UriFormat.Unescaped).TrimEnd('/');
            foreach (DataRow row in selectedRows)
            {
                try
                {
                    string error = null;
                    string fileUrl = hostUrl + "/" + Utility.ParseLookupValue(row["FileRef"].ToString()).TrimStart('/');
                    switch (checkOutAction)
                    {
                        case CheckOutAction.CheckOut: if (!WscHelper.Current.CheckoutFile (this.Settings, fileUrl)) error = "Checkout failed."; break;
                        case CheckOutAction.UndoCheckOut: if (!WscHelper.Current.UndoCheckoutFile(this.Settings, fileUrl)) error = "Undo checkout failed."; break;
                        case CheckOutAction.CheckInMinor:
                        case CheckOutAction.CheckInMajor:
                        case CheckOutAction.CheckInOverwrite: if (!WscHelper.Current.CheckinFile(this.Settings, fileUrl, checkOutAction)) error = "Checkin failed."; break;
                    }
                    if (error == null) row.ClearErrors();
                    else row.RowError = error;
                    succeededRows.Add(row);
                }
                catch (Exception ex)
                {
                    row.RowError = ex.InnerException != null ? ex.InnerException.Message : ex.Message;
                }
            }
            this.ApplyResyncChanges(list, succeededRows);
        }

        public void Moderate(List<DataRow> selectedRows, ModerationStatus moderationStatus)
        {
            if (selectedRows == null || selectedRows.Count == 0) return;

            foreach (DataRow row in selectedRows) row["_ModerationStatus"] = (int)moderationStatus;
            this.UpdateItems(selectedRows[0].Table, selectedRows.ToArray(), true);
        }

        #endregion

        #region private

        private void UpdateItems (DataTable table, DataRow[] selectedRows, bool moderate)
        {
            ListData list = WscHelper.Current.GetListData(this.Settings);
            Dictionary<string, DataRow> rowMap = new Dictionary<string, DataRow>();
            int nextMethodID = 0;

            List<string> results = new List<string>();
            foreach (DataRow[] partSelectedRows in Utility.SplitInChunks<DataRow>(selectedRows, NUMBER_BATCH_ITEMS))
            {
                string batch = this.GenerateCAML(table, partSelectedRows, list, rowMap, moderate, ref nextMethodID);
                XElement result = WscHelper.Current.SaveListItemData(this.Settings, batch);
                results.Add(result.ToString());
            }
            this.ResyncAfterUpdate(MergeResultXMLs(results.ToArray()), list, rowMap);
        }

        private string MergeResultXMLs(string[] results)
        {
            if (results.Length == 0) return string.Empty;
            if (results.Length == 1) return results[0];

            XElement el = XElement.Parse(results[0]);
            el.Add(results.Skip(1).SelectMany(r => XElement.Parse(r).Elements().Where(e => e.Name.LocalName == "Result")));
            return el.ToString();
        }

        private string GenerateCAML(DataTable table, DataRow[] changedRows, ListData list, Dictionary<string, DataRow> rowMap, bool moderate, ref int nextMethodID)
        {
            Dictionary<string, FieldData> fieldMap = list.Fields.ToDictionary(f => f.Name);

            StringBuilder sb = new StringBuilder();
            string rootFolderAttr = string.IsNullOrEmpty(this.Settings.RootFolder) ? string.Empty : " RootFolder=\"" + Utility.EscapeForXml(this.Settings.RootFolder) + "\"";
            sb.Append("<Batch OnError=\"Continue\"" + rootFolderAttr + ">");

            List<string> columnNames = table.Columns.Cast<DataColumn>().Where(c => !Utility.UIReadonlyFields.Contains(c.ColumnName) && !SpecialFields.Contains(c.ColumnName)).Select(c => c.ColumnName).ToList();
            bool isLibrary = list.IsDocLib;
            bool hasFileLeafRef = table.Columns.Contains("FileLeafRef");
            bool hasTitle = table.Columns.Contains("Title");
            string listServerUrl = list.RootFolder;
            string rootFolder = this.Settings.RootFolder;
            string folderBaseUrl = (string.IsNullOrEmpty(rootFolder) ? listServerUrl : rootFolder).TrimEnd('/') + "/";
            
            foreach (DataRow row in changedRows)
            {
                string id = row.HasVersion(DataRowVersion.Original) ? row["ID", DataRowVersion.Original].ToString() : row["ID"].ToString();
                string methodID = "M" + nextMethodID;
                string commandName = row.RowState == DataRowState.Deleted ? "Delete" : (moderate ? "Moderate" : (row.RowState == DataRowState.Added ? "New" : "Update"));
                rowMap[methodID] = row;

                if (string.IsNullOrEmpty(id)) id = "New";
                sb.AppendFormat("<Method ID=\"{0}\" Cmd=\"{1}\">", methodID, commandName);
                AddFieldElement(sb, "ID", id);

                if (row.RowState == DataRowState.Added)
                {
                    foreach (string columnName in columnNames)
                    {
                        if (Utility.RequiredFields.Contains(columnName)) continue;
                        string columnValue = this.GetColumnValue(row, columnName, fieldMap);
                        AddFieldElement(sb, columnName, columnValue);
                    }

                    string fsObjType = Utility.ParseLookupValue(row["FSObjType"].ToString());
                    if ("1".Equals(fsObjType))
                    {
                        AddFieldElement(sb, "FSObjType", "1");
                        string name = string.Empty;
                        if (hasFileLeafRef) name = Utility.ParseLookupValue(row["FileLeafRef"].ToString());
                        if (string.IsNullOrEmpty(name) && hasTitle) name = row["Title"].ToString();
                        AddFieldElement(sb, "FileRef", folderBaseUrl + name);
                    }
                }
                else
                {
                    bool isFolder = false;
                    if (row.RowState == DataRowState.Modified)
                    {
                        string fsObjType = Utility.ParseLookupValue(row["FSObjType", DataRowVersion.Original].ToString());
                        isFolder = "1".Equals(fsObjType);
                    }

                    if (isLibrary || isFolder)
                    {
                        string fileRef = Utility.ParseLookupValue(row["FileRef", DataRowVersion.Original].ToString());
                        if (!fileRef.StartsWith("/")) fileRef = "/" + fileRef;
                        AddFieldElement(sb, "FileRef", fileRef);
                    }

                    if (row.RowState == DataRowState.Modified)
                    {
                        // add the required fields aliases
                        if ((isLibrary || isFolder) && hasFileLeafRef && !row["FileLeafRef", DataRowVersion.Original].Equals(row["FileLeafRef"]))
                        {
                            AddFieldElement(sb, "BaseName", Utility.GetFileNameWOExt(Utility.ParseLookupValue(row["FileLeafRef"].ToString())));

                            // the only folder in a list that this will be able to rename is the one with ID = 1
                            // due to a bug in the STSSOAP assembly this line will get translated to something like - <SetVar Name="Type">21;#1</SetVar> - (for ID = 21), where it should be just <SetVar Name="Type">1</SetVar>
                            // so - renaming of folders in lists will be only possible with the object model
                            if (isFolder) AddFieldElement(sb, "FSObjType", "1");
                        }

                        if (!DBNull.Value.Equals(row["_ModerationStatus"])) AddFieldElement(sb, "_ModerationStatus", row["_ModerationStatus"].ToString());
                        if (!DBNull.Value.Equals(row["_Level"])) AddFieldElement(sb, "_Level", row["_Level"].ToString());

                        AddFieldElement(sb, "owshiddenversion", row["owshiddenversion"].ToString());

                        foreach (string columnName in columnNames)
                        {
                            string columnValue = this.GetColumnValue(row, columnName, fieldMap); ;
                            if (!columnValue.Equals(this.GetColumnValue(row, columnName, fieldMap, DataRowVersion.Original)))
                                AddFieldElement(sb, columnName, columnValue);
                        }
                    }
                }

                sb.Append("</Method>");
                nextMethodID++;
            }
            sb.Append("</Batch>");
            return sb.ToString();
        }

        private void ResyncAfterUpdate(string result, ListData list, Dictionary<string, DataRow> rowMap)
        {
            XDocument resultsDoc = XDocument.Parse(result);

            bool resyncAfterUpdate = this.Settings.ResyncAfterUpdate;
            List<XElement> rowElements = new List<XElement>();
            Dictionary<string, DataRow> idRowMap = new Dictionary<string, DataRow>();

            foreach (XElement el in resultsDoc.Root.Elements().Where(e => e.Name.LocalName == "Result"))
            {
                string methodID = el.Attribute("ID").Value.Split(',')[0];
                string code = el.Elements().Where(e => e.Name.LocalName == "ErrorCode").First().Value;
                if (code.StartsWith("0x")) code = code.Substring(2);
                long nCode;
                if (!long.TryParse(code, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out nCode)) continue;
                if (!rowMap.ContainsKey(methodID)) continue;

                DataRow row = rowMap[methodID];
                if (nCode == 0)
                {
                    if (row.RowState == DataRowState.Added || resyncAfterUpdate)
                    {
                        XElement rowEl = el.Elements().Where(e => e.Name.LocalName == "row").FirstOrDefault();
                        if (rowEl != null && rowEl.Attribute("ows_ID") != null)
                        {
                            if (row.RowState == DataRowState.Added) row["ID"] = rowEl.Attribute("ows_ID").Value;
                            if (resyncAfterUpdate)
                            {
                                idRowMap[row["ID"].ToString()] = row;
                                rowElements.Add(rowEl);
                            }
                        }
                    }
                    row.ClearErrors();
                    row.AcceptChanges();
                }
                else
                {
                    if (row.RowState == DataRowState.Deleted) row.RejectChanges();
                    row.RowError = el.Value;
                }
            }
            if (!resyncAfterUpdate) return;

            DataTable newTable = WscHelper.Current.GetTableFromRowElements(list, rowElements, Utility.GetViewFieldWithRequired(this.Settings.ViewFields, list.IsDocLib));
            foreach (DataRow newRow in newTable.Rows)
            {
                string id = newRow["ID"].ToString();
                if (!idRowMap.ContainsKey(id)) continue;
                DataRow row = idRowMap[id];
                row.ItemArray = newRow.ItemArray;
                row.AcceptChanges();
            }
        }

        protected void ApplyResyncChanges(ListData list, List<DataRow> resyncRows)
        {
            // resync in batches - too many CAML OR-s lead to exception
            foreach (DataRow[] partResyncRows in Utility.SplitInChunks<DataRow>(resyncRows.ToArray(), NUMBER_BATCH_ITEMS))
            {
                string idQuery = Utility.GetQueryByIDs(partResyncRows.Select(r => r["ID"].ToString()).ToList());
                GridViewDataSourceSettings newSettings = this.Settings.Clone();
                newSettings.Query = idQuery;
                // force recursive all
                newSettings.ViewScope = ViewScope.RecursiveAll;
                // force nil root folder
                newSettings.RootFolder = null;

                DataTable newTable = WscHelper.Current.GetListItemData(newSettings, list, Guid.Empty, true);

                Dictionary<string, DataRow> newRowsMap = newTable.Rows.Cast<DataRow>().ToDictionary(r => r["ID"].ToString());
                foreach (DataRow row in partResyncRows)
                {
                    string strID = row["ID"].ToString();
                    if (newRowsMap.ContainsKey(strID))
                    {
                        row.ItemArray = newRowsMap[strID].ItemArray;
                    }
                    row.AcceptChanges();
                }
            }
        }

        private void AddFieldElement(StringBuilder sb, string name, string value)
        {
            sb.AppendFormat("<Field Name=\"{0}\">{1}</Field>", Utility.EscapeForXml(name), Utility.EscapeForXml(value));
        }
        private string GetColumnValue(DataRow row, string columnName, Dictionary<string, FieldData> fieldMap)
        {
            return GetColumnValue(row, columnName, fieldMap, DataRowVersion.Current);
        }

        private string GetColumnValue(DataRow row, string columnName, Dictionary<string, FieldData> fieldMap, DataRowVersion version)
        {
            DateTime date;
            object obj = row[columnName, version];
            string strVal = obj.ToString();

            FieldData field = fieldMap[columnName];
            if (field.FieldType == FieldType.DateTime || (field.ColName != null && field.ColName.StartsWith("datetime")))
            {
                if (obj is DateTime)
                {
                    date = (DateTime)obj;
                    goto formatdate;
                }
                else if (obj is string)
                {
                    if (DateTime.TryParse(strVal, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
                    {
                        goto formatdate;
                    }
                }
            }
            else if (obj is double) strVal = ((double)obj).ToString(CultureInfo.InvariantCulture);
            else if (obj is int) strVal = ((int)obj).ToString(CultureInfo.InvariantCulture);

            return strVal;
        formatdate:
            return date.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
        }

        private List<string> FixViewFieldSchema(ListData listData, List<string> viewFields)
        {
            if (viewFields == null) viewFields = new List<string>();
            Dictionary<string, FieldData> fieldMap = listData.Fields.ToDictionary(f => f.Name);

            List<string> newViewFields = new List<string>();

            HashSet<string> fieldNames = new HashSet<string>();
            for (int i = 0; i < viewFields.Count; i++)
            {
                string name = viewFields[i];
            recheck:
                if (fieldNames.Contains(name)) continue;

                FieldData field = fieldMap.ContainsKey(name) ? fieldMap[name] : null;
                if (field == null) continue;

                if (field.FieldType == FieldType.Computed 
#if SHAREPOINT2010
                    && field.Name != "ContentType"
#endif
                    )
                {
                    if (field.FieldRefs != null && field.FieldRefs.Count > 0)
                    {
                        name = field.FieldRefs[0];
                        goto recheck;
                    }
                    continue;
                }
                else if (IsFieldTypeNotAllowed(field)) continue;

                newViewFields.Add(name);
                fieldNames.Add(name);
            }
            return newViewFields;
        }

        private bool IsFieldTypeNotAllowed(FieldData fieldData)
        {
            return fieldData.FieldType == FieldType.Calculated;
        }

        #endregion
    }
}
